package br.cin.ufpe.corba;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;

import br.cin.ufpe.corba.iiop.MessageHeader;
import br.cin.ufpe.corba.iiop.MsgType;
import br.cin.ufpe.corba.iiop.Reply;
import br.cin.ufpe.corba.iiop.Request;
import br.cin.ufpe.corba.interfacerepository.InterfaceDef;
import br.cin.ufpe.corba.namingservice.InvalidName;
import br.cin.ufpe.corba.namingservice.NamingContext;
import br.cin.ufpe.corba.namingservice.NamingContextExt;

public class ORB {
	
	private int port;	
	private HashMap<String, Object> initialReferences;
	private HashMap<String, Servant> servants;
	private Listener listener;	
	
	private ORB(int port, String namingServiceHost, int namingServicePort) throws IOException {
		this.port = port;
		this.servants = new HashMap<String, Servant>();
		this.initialReferences = new HashMap<String, Object>();

		// Naming Service ROOT
		ObjectRef objectRef = new ObjectRef(this);
		objectRef.setObjectAddress(new ObjectAddress(namingServiceHost, namingServicePort, "NamingContext"));
		objectRef.set_interface(new InterfaceDef(NamingContextExt.class.getCanonicalName()));
		objectRef.get_interface().getBaseInterfaces().add(NamingContext.class.getCanonicalName());
		this.initialReferences.put("NameService", objectRef);
		
		this.listener = new Listener(port);
		new Thread(listener).start();
	}
	
	public static ORB init() throws NumberFormatException, IOException {
		return init(2001, "localhost", 1133);
	}
	
	public static ORB init(int port, String namingServiceHost, int namingServicePort) throws NumberFormatException, IOException {
		return new ORB(port, namingServiceHost, namingServicePort);
	}
	
	public static ORB init(String[] args) throws NumberFormatException, IOException {
		return new ORB(Integer.parseInt(args[0]), args[1], Integer.parseInt(args[2]));
	}

	public void destroy() {
		this.listener.stop();
		this.servants.clear();
	}
	
	public Object resolve_initial_references(String name) throws InvalidName {
		if (!initialReferences.containsKey(name)) {
			throw new InvalidName();
		}
		
		return initialReferences.get(name);
	}
	
	public void add(Servant skeleton) {
		String key = skeleton.getClass().getName() + this.servants.size();
		add(key, skeleton);
	}
	
	public void add(String key, Servant skeleton) {		
		skeleton.setObjectAddress(new ObjectAddress(getORBAddress(), key));
		this.servants.put(key, skeleton);
	}
	
	public Servant getObject(String key) {
		return this.servants.get(key);
	}
	
	public Reply invoke(ObjectAddress address, Request request) throws IOException, ClassNotFoundException {		
		// Open the connection
		Socket socket = new Socket(address.getHost(), address.getPort());
		
		// Send request
		MessageHeader header = new MessageHeader(MsgType.Request);
		header.write(socket.getOutputStream());
		request.write(socket.getOutputStream());
		
		// Receive reply
		header = MessageHeader.read(socket.getInputStream());
		if (header.getMessageType() != MsgType.Reply) {
			throw new IOException("Invalid message type. Reply expected");
		}		
		Reply reply = Reply.read(socket.getInputStream());
		
		// Close the connection
		socket.close();
		
		return reply;
	}
	
	private Reply processRequest(Request request) {
		Servant servant = null;
		Reply reply = null;
		
		if (servants.containsKey(request.getHeader().getObjectKey())) {
			servant = servants.get(request.getHeader().getObjectKey());
			reply = servant.processRequest(request);
		} else {
			reply = new Reply(request, new ObjectNotFound(request.getHeader().getObjectKey()));
		}
		
		return reply;
	}
	
	private class Listener implements Runnable {
		
		private ServerSocket listener;
		private boolean stop;
		
		public Listener(int port) throws IOException {
			this.listener = new ServerSocket(port);
		}
		
		public void run() {
			while (!stop) {
				try {
					Socket socket = this.listener.accept();
					MessageHeader header = MessageHeader.read(socket.getInputStream()); 
					if (header.getMessageType() == MsgType.Request) {
						Request request = Request.read(socket.getInputStream());
						Reply reply = processRequest(request);
						header = new MessageHeader(MsgType.Reply);
						header.write(socket.getOutputStream());
						reply.write(socket.getOutputStream());
					}
				} catch (IOException e) {
					e.printStackTrace();
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}
			}			
		}		
		
		public void stop() {
			this.stop = true;
			try {
				this.listener.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public ORBAddress getORBAddress() {		
		try {
			return new ORBAddress(InetAddress.getLocalHost().getHostName(), port);
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		
		return null;
	}

	public boolean ping(ObjectAddress objectAddress) {
		// TODO Auto-generated method stub
		return false;
	}
}
